package com.luych.toolbox.common.xml;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class XmlNodeWriter {

    private static final Logger logger = LoggerFactory.getLogger(XmlNodeWriter.class);

    private final XmlNode node;

    public static XmlNodeWriter empty() {
        return new XmlNodeWriter(null);
    }

    public static XmlNodeWriter instance(String name) {
        return new XmlNodeWriter(new XmlNode(name));
    }

    public XmlNodeWriter(XmlNode node) {
        this.node = node;
    }

    public XmlNodeWriter getChildOrThrowException(String childName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        XmlNode xmlNode = this.node.getChildren().stream().filter(node -> node.getName().equals(childName)).findFirst()
            .orElseThrow(() -> new RuntimeException(this.node.getPath() + "." + childName + " not found"));
        return new XmlNodeWriter(xmlNode);
    }

    public XmlNodeWriter getChildOrLogWarn(String childName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        Optional<XmlNode> xmlNode = this.node.getChildren().stream().filter(node -> node.getName().equals(childName)).findFirst();
        if (!xmlNode.isPresent()) {
            logger.warn("{}.{} not found, and it will return an empty XmlNodeWriter", this.node.getPath(), childName);
        }
        return XmlNodeWriter.empty();
    }

    public XmlNodeWriter getChildOrCreateBefore(String childName, String beforeName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        return this.node.getChildren().stream().filter(node -> node.getName().equals(childName))
            .findFirst().map(XmlNodeWriter::new).orElseGet(() -> this.createChildBefore(childName, beforeName));
    }

    public XmlNodeWriter getChildOrCreateAfter(String childName, String afterName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        return this.node.getChildren().stream().filter(node -> node.getName().equals(childName))
            .findFirst().map(XmlNodeWriter::new).orElseGet(() -> this.createChildAfter(childName, afterName));
    }

    public XmlNodeWriter getChildOrCreateAtHeader(String childName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        return this.node.getChildren().stream().filter(node -> node.getName().equals(childName))
            .findFirst().map(XmlNodeWriter::new).orElseGet(() -> this.createChildAtHeader(childName));
    }

    public XmlNodeWriter getChildOrCreateAtBottom(String childName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        return this.node.getChildren().stream().filter(node -> node.getName().equals(childName))
            .findFirst().map(XmlNodeWriter::new).orElseGet(() -> this.createChildAtBottom(childName));
    }

    public XmlNodeWriter createChildBefore(String childName, String beforeName) {
        return this.addChildBefore(new XmlNode(childName), beforeName);
    }

    public XmlNodeWriter createChildAfter(String childName, String afterName) {
        return this.addChildAfter(new XmlNode(childName), afterName);
    }

    public XmlNodeWriter createChildAtHeader(String name) {
        return this.addChildAtHeader(new XmlNode(name));
    }

    public XmlNodeWriter createChildAtBottom(String name) {
        return this.addChildAtBottom(new XmlNode(name));
    }

    public XmlNodeWriter addChildBefore(XmlNode childNode, String beforeName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        childNode.setParent(this.node);
        List<XmlNode> beforeNodes = this.node.getChildren().stream().filter(node -> node.getName().equals(beforeName)).collect(Collectors.toList());
        if (beforeNodes.size() > 0) {
            int beforeNodeIndex = this.node.getChildren().indexOf(beforeNodes.get(0));
            this.node.getChildren().add(beforeNodeIndex, childNode);
        } else {
            logger.warn("{}.{} not found, and it will create child at header", this.node.getPath(), childNode.getName());
            this.node.getChildren().add(0, childNode);
        }
        return new XmlNodeWriter(childNode);
    }

    public XmlNodeWriter addChildAfter(XmlNode childNode, String afterName) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        childNode.setParent(this.node);
        List<XmlNode> afterNodes = this.node.getChildren().stream().filter(node -> node.getName().equals(afterName)).collect(Collectors.toList());
        if (afterNodes.size() > 0) {
            int afterNodeIndex = this.node.getChildren().indexOf(afterNodes.get(afterNodes.size() - 1));
            this.node.getChildren().add(afterNodeIndex + 1, childNode);
        } else {
            logger.warn("{}.{} not found, and it will create child at bottom", this.node.getPath(), childNode.getName());
            this.node.getChildren().add(childNode);
        }
        return new XmlNodeWriter(childNode);
    }

    public XmlNodeWriter addChildAtHeader(XmlNode xmlNode) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        xmlNode.setParent(this.node);
        this.node.getChildren().add(0, xmlNode);
        return new XmlNodeWriter(xmlNode);
    }

    public XmlNodeWriter addChildAtBottom(XmlNode xmlNode) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        xmlNode.setParent(this.node);
        this.node.getChildren().add(xmlNode);
        return new XmlNodeWriter(xmlNode);
    }

    public XmlNodeWriter value(Object value) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        this.node.setValue(Optional.ofNullable(value).map(Object::toString).orElse(null));
        return this;
    }

    public XmlNodeWriter attribute(String key, Object value) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        this.node.getAttrs().put(key, Optional.ofNullable(value).map(Object::toString).orElse(null));
        return this;
    }

    public XmlNodeWriter valueIfPresent(Object value) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        Optional.ofNullable(value).map(Object::toString).ifPresent(this.node::setValue);
        return this;
    }

    public XmlNodeWriter attributeIfPresent(String key, Object value) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        Optional.ofNullable(value).map(Object::toString).ifPresent(s -> node.getAttrs().put(key, s));
        return this;
    }

    public XmlNodeWriter clear(String... ignoreAttributes) {
        if (this.node == null) {
            logger.warn("Current XmlNodeWriter is empty, ignore and return this");
            return this;
        }
        this.node.getChildren().forEach(child -> child.safeWrite().clear(ignoreAttributes));
        this.node.getChildren().removeIf(child -> child.isEmpty(ignoreAttributes));
        return this;
    }

    public XmlNode get() {
        return this.node;
    }
}
